FireLens(Fluent Bit)からCloudWatch Logsへログ送信に必要な最小権限のタスクロールを作成し動作検証してみた
ECSのタスク(コンテナ)のログ出力先を変更できるFireLens機能があります。本記事ではFluent Bitを利用したカスタムログルーティングでCloudWatch Logsへアプリケーションコンテナのログを送ります。CloudWatch Logsへ送信するための最小権限と、送信先のCloudWatch Logsのロググループ名を限定したIAMポリシーを作成します。動作検証しましたのでまとめます。
-
Icons made by Freepik from www.flaticon.com
FireLensイメージの準備
Fluent Bitでカスタムログルーティングするための設定ファイルを作成します。設定ファイルを同梱したFireLensイメージを作成しECRにプッシュします。
- すべてのログをCloudWatch Logsへ送る
- ロググループ名は
/ecs/logs/sample-test
- ここで指定したロググループ名は後で必要になります。
- ログストリーム名は
webapp
- ロググループが存在していない場合は新規作成する
[SERVICE] Flush 1 Grace 30 Log_Level info [OUTPUT] Name cloudwatch_logs Match * region ap-northeast-1 log_group_name /ecs/logs/sample-test log_stream_name webapp auto_create_group true
完成イメージ
ベースのイメージはAmazonが配布しているAWS用のプラグインが同梱されたFluent Bitのイメージを利用します。カスタムログルーティング用の設定ファイルをイメージ内にコピーします。
FROM amazon/aws-for-fluent-bit:2.18.0 COPY ./extra.conf /fluent-bit/etc/extra.conf
設定ファイル込みのイメージをECRへプッシュして準備完了です。
docker build -t sample-test-custom-firelens:v1 . docker tag sample-test-custom-firelens:v1 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/sample-test-custom-firelens:v1 docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/sample-test-custom-firelens:v1
CloudFormation
テンプレートを実行すると以下のFargateでの動作確認環境を構築できます。VPCは事前に用意してください。
-
Icons made by Freepik from www.flaticon.com
テンプレート
折りたたみ
AWSTemplateFormatVersion: "2010-09-09" Description: Create Fargate*1, Firelens*1 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Common Settings Parameters: - ProjectName - Environment - Label: default: ECS VPC Settings Parameters: - VPCID - PublicSubnet1 - PublicSubnet2 - PrivateSubnet1 - PrivateSubnet2 Parameters: ProjectName: Description: Project Name Type: String Default: unnamed Environment: Description: Environment Type: String Default: dev AllowedValues: - prod - dev - stg - test VPCID: Type: AWS::EC2::VPC::Id PublicSubnet1: Description: "Web App Subnet 1st" Type: AWS::EC2::Subnet::Id PublicSubnet2: Description: "Web App Subnet 2nd" Type: AWS::EC2::Subnet::Id PrivateSubnet1: Description: "ECS Subnet 1st" Type: AWS::EC2::Subnet::Id PrivateSubnet2: Description: "ECS Subnet 2nd" Type: AWS::EC2::Subnet::Id DesiredCount: Type: Number Default: 1 ClusterName: Type: String Default: cluster AppName: Type: String Default: webapp ServiceName: Type: String Default: service TaskDefinitionName: Type: String Default: taskdefinition ImageNameWebApp: Description: "Web Application Repository Name also Need to TagName" Type: String Default: "public.ecr.aws/nginx/nginx:latest" ImageNameFirelens: Description: "Firelens Repository Name also Need to TagName" Type: String Default: "public.ecr.aws/aws-observability/aws-for-fluent-bit:latest" Resources: # -------------------------------------------- # ELB # -------------------------------------------- ELB1: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Type: "application" Name: !Sub ${ProjectName}-${Environment}-elb Scheme: "internet-facing" SecurityGroups: - !Ref SecurityGroup2 Subnets: - !Ref PublicSubnet1 - !Ref PublicSubnet2 IpAddressType: "ipv4" Tags: - Key: Name Value: !Sub ${ProjectName}-${Environment}-elb ELBListener1: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - TargetGroupArn: !Ref TargetGroup1 Type: "forward" LoadBalancerArn: !Ref ELB1 Port: 80 Protocol: "HTTP" TargetGroup1: Type: "AWS::ElasticLoadBalancingV2::TargetGroup" Properties: VpcId: !Ref VPCID Name: !Sub ${ProjectName}-${Environment}-tg Protocol: "HTTP" HealthCheckPath: "/" Port: 80 TargetType: ip HealthCheckIntervalSeconds: 10 # Default is 30. HealthyThresholdCount: 2 # Default is 5. HealthCheckTimeoutSeconds: 5 UnhealthyThresholdCount: 2 TargetGroupAttributes: - Key: "stickiness.enabled" Value: "false" - Key: deregistration_delay.timeout_seconds Value: "60" # default is 300. - Key: "stickiness.type" Value: "lb_cookie" - Key: "stickiness.lb_cookie.duration_seconds" Value: "86400" - Key: "slow_start.duration_seconds" Value: "0" - Key: "load_balancing.algorithm.type" Value: "round_robin" # -------------------------------------------- # CloudWatch Logs Group # -------------------------------------------- # FireLens Stdout FireLensLogGroup: Type: "AWS::Logs::LogGroup" Properties: LogGroupName: !Sub "/ecs/logs/${ProjectName}-${Environment}-firelens" # -------------------------------------------- # ECS Fargate # -------------------------------------------- # Cluster ECSCluster: Type: "AWS::ECS::Cluster" Properties: ClusterName: !Sub "${ProjectName}-${Environment}-${ClusterName}" CapacityProviders: - "FARGATE_SPOT" - "FARGATE" # Service ECSService: Type: "AWS::ECS::Service" Properties: ServiceName: !Sub ${ProjectName}-${Environment}-${ServiceName} Cluster: !Ref ECSCluster LaunchType: "FARGATE" PlatformVersion: "1.4.0" DesiredCount: !Ref DesiredCount LoadBalancers: - TargetGroupArn: !Ref TargetGroup1 ContainerName: !Ref AppName ContainerPort: 80 NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: "DISABLED" SecurityGroups: - !Ref SecurityGroup1 Subnets: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 TaskDefinition: !Ref ECSTaskDefinition DependsOn: ELBListener1 # ECS TaskDefinition ECSTaskDefinition: Type: "AWS::ECS::TaskDefinition" Properties: Family: !Sub "${ProjectName}-${Environment}-${AppName}-${TaskDefinitionName}" TaskRoleArn: !GetAtt ECSTaskRole1.Arn ExecutionRoleArn: !GetAtt ECSTaskExecutionRole1.Arn NetworkMode: "awsvpc" RequiresCompatibilities: - "FARGATE" Cpu: "256" Memory: "512" ContainerDefinitions: - Essential: true Name: !Ref AppName Image: !Ref ImageNameWebApp LogConfiguration: LogDriver: "awsfirelens" PortMappings: - ContainerPort: 80 HostPort: 80 Protocol: "tcp" - Essential: true Name: "log_router" Image: !Ref ImageNameFirelens LogConfiguration: LogDriver: "awslogs" Options: awslogs-group: !Ref FireLensLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: "firelens" FirelensConfiguration: Type: "fluentbit" Options: config-file-type: "file" config-file-value: "/fluent-bit/etc/extra.conf" User: "0" # -------------------------------------------- # Security Group # -------------------------------------------- # Security Group for WebApp SecurityGroup1: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub ${ProjectName}-${Environment}-${AppName}-sg GroupDescription: Web App Security Group SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 SourceSecurityGroupId: !Ref SecurityGroup2 Description: "Access from ELB" VpcId: !Ref VPCID Tags: - Key: Name Value: !Sub ${ProjectName}-${Environment}-${AppName}-sg # Security Group for ELB SecurityGroup2: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub ${ProjectName}-${Environment}-elb-sg GroupDescription: ELB Security Group SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: "0.0.0.0/0" Description: "Access from Public" VpcId: !Ref VPCID Tags: - Key: Name Value: !Sub ${ProjectName}-${Environment}-elb-sg # -------------------------------------------- # IAM Role # -------------------------------------------- # ECS Task Execution Role ECSTaskExecutionRole1: Type: "AWS::IAM::Role" Properties: RoleName: !Sub ${ProjectName}-${Environment}-${AppName}-ECSTaskExecutionRole Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy # ECS Task Role ECSTaskRole1: Type: "AWS::IAM::Role" Properties: Path: "/" RoleName: !Sub ${ProjectName}-${Environment}-${AppName}-ECSTaskRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - !Ref ECSExecPolicy - !Ref SentCloudWatchLogsPolicy # -------------------------------------------- # IAM Policy # -------------------------------------------- # Allowed ECS Exec for Task Role ECSExecPolicy: Type: "AWS::IAM::ManagedPolicy" Properties: ManagedPolicyName: !Sub "${ProjectName}-${Environment}-ECSExecPolicy" Path: "/" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - ssmmessages:CreateControlChannel - ssmmessages:CreateDataChannel - ssmmessages:OpenControlChannel - ssmmessages:OpenDataChannel Resource: "*" # Sent CloudWatch Logs for Task Role SentCloudWatchLogsPolicy: Type: "AWS::IAM::ManagedPolicy" Properties: ManagedPolicyName: !Sub "${ProjectName}-${Environment}-SentCloudWatchLogsPolicy" Path: "/" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:CreateLogGroup - logs:DescribeLogStreams - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/logs/sample-test:*
検証環境
項目 | バージョン |
---|---|
aws-for-fluent-bit | 2.18.0 |
Fluent Bit | 1.8.2 |
Fargate platform | 1.4.0 |
ポイント
- ELB経由でNginxコンテナにアクセスすることで、アクセスログがFireLensコンテナ経由でCloudWatch Logsへ保存
- CloudWatch Logsへログを送るために必要なタスクロールの権限をできる限り制限
- FireLensのイメージは事前にカスタムログルーティングの設定ファイルを同梱して準備が必要
- Nginxコンテナは素のイメージをそのまま使用するので準備不要
- セキュリティグループが0.0.0.0/0で解放されています、必要に応じてアクセス元を制限してください
FireLensイメージのパラメータ指定
FireLensのイメージ名はECRへプッシュした設定ファイルが同梱したイメージを指定してください。デフォルト値は素のFireLens(Fluent Bit)のイメージになっています。
FilreLensの設定
FireLensのログ設定周りを見ていきます。
アプリコンテナ
- LogCongurationのLogDriverの指定は
awsfirelens
Name: !Ref AppName Image: !Ref ImageNameWebApp LogConfiguration: LogDriver: "awsfirelens" PortMappings: - ContainerPort: 80 HostPort: 80 Protocol: "tcp"
FireLensコンテナ
- LogConfigurationはFireLensコンテナの標準出力内容をデバッグ用にCloudWatch Logsへ送ります。
- FirelensConfigurationにはカスタム設定ファイルをDockerイメージ内にコピー先のパスを指定します。
Name: "log_router" Image: !Ref ImageNameFirelens LogConfiguration: LogDriver: "awslogs" Options: awslogs-group: !Ref FireLensLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: "firelens" FirelensConfiguration: Type: "fluentbit" Options: config-file-type: "file" config-file-value: "/fluent-bit/etc/extra.conf
タスクロール用のIAMポリシー設定
FireLensからCloudWatch Logsへログを送るためにはタスクロールに権限が必要です。Actionは最低限必要な権限に絞り、Resourceで送信先のロググループも限定します。
Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/logs/sample-test:*
の/ecs/logs/sample-test
が注意するところです。ここはロググループ名を指しており、FireLensからの送信先ロググループ名はFluent Bitの設定ファイル(extra.conf)でロググループ名を決めています。
# Sent CloudWatch Logs for Task Role SentCloudWatchLogsPolicy: Type: "AWS::IAM::ManagedPolicy" Properties: ManagedPolicyName: !Sub "${ProjectName}-${Environment}-SentCloudWatchLogsPolicy" Path: "/" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:CreateLogGroup - logs:DescribeLogStreams - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/logs/sample-test:*
FireLensイメージ内に同梱した設定ファイルを再掲します。IAMポリシーでResourceも制限するときはlog_group_name
で指定した名前を確認しましょう。
[SERVICE] Flush 1 Grace 30 Log_Level info [OUTPUT] Name cloudwatch_logs Match * region ap-northeast-1 log_group_name /ecs/logs/sample-test log_stream_name webapp auto_create_group true
Resourceは制限しないで、Actionだけを純粋にみると以下のIAMポリシーになります。一部Createが必要になるのはFluent Bitの設定ファイルでauto_create_group
がtrueだと、指定したロググループと、ログストリームを自動的に新規作成してくれるためです。
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:CreateLogGroup", "logs:DescribeLogStreams", "logs:PutLogEvents" ], "Resource": "*" }] }
完成イメージ
テンプレートを実行をすると以下のIAMポリシーが生成されます。
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:CreateLogGroup", "logs:DescribeLogStreams", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:<loggroup-name>:*" }] }
動作確認
ELBのDNS名にアクセスするとNginxのページが返ってきます。
Nginxのログ確認
アプリケーションコンテナのログはFluent Bitの設定ファイルで指定したロググループにログが保存される予定です。今回の検証環境では/etc/logs/sample-test/webapp
にログが保存されます。
ELBのヘルスチェックのログを確認できます。検証の目的であったタスクロールのIAMポリシー設定に問題ないことを確認できました。
タスクロールの権限を間違った場合
CloudWatch Logsへ送信用のActionsとResource指定に問題ないことを確認できました。仮にResourceの指定を誤っていた場合、切り分けするにはどこを確認したらよいのか確認しておきます。
Resourceで指定したCloudWatch Logsのロググループ名を適当な名前に変更しました。先ほど送信していたロググループ名へアクセスできない状態にしました。
FireLens経由で送信されるCloudWatch Logsのロググループ(/etc/logs/sample-test/webapp
)のログが止まりました。時刻はIAMポリシーを変更して間もない22:00:12で止まっています。ログが止まったということはわかりました。なにが原因で止まったのかはアプリケーションコンテナのログからでは判斷する材料がありません。
FireLensの標準出力結果を送信しているCloudWatch Logsのロググループ(/etc/logs/sample-test-firelens
)を確認します。
アプリケーションコンテナのログが止まった直後の22:00:13にAccessDeniedExceptionのメッセージ確認できました。アクセス権がないということから、タスクロールのIAMポリシーなど権限周りを疑うことができます。
FireLensコンテナのログ設定はテンプレートの以下の箇所で指定しています。明示的にCloudWatch Logsのロググループをテンプレート内で作成しています。
# FireLens Stdout FireLensLogGroup: Type: "AWS::Logs::LogGroup" Properties: LogGroupName: !Sub "/ecs/logs/${ProjectName}-${Environment}-firelens" ...snip... Name: "log_router" Image: !Ref ImageNameFirelens LogConfiguration: LogDriver: "awslogs" Options: awslogs-group: !Ref FireLensLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: "firelens" FirelensConfiguration: Type: "fluentbit" Options: config-file-type: "file" config-file-value: "/fluent-bit/etc/extra.conf
アプリケーションコンテナのログ送信先のロググループ名はCloudFormationのテンプレートからは作成していません。Fluent Bitの設定ファイルで対象のロググループ名がない場合は新規作成できるため、Fluent Bitの設定ファイルに寄せています。
おわりに
IAMポリシーのActionはよいとして、Resourceの指定はFluent Bitから作成されるリソース(CloudWatch Logsのロググループ)をCloudFormationのテンプレートで作成するIAMポリシーを指定しています。テンプレートを一発実行して何か問題にならないのか気になったので検証環境を作成しました。どなたかの参考になれば幸いです。